安全管理(zero-rbac)

白皮书中的权限管理主要包括:

  • 全局配置:zero-rbac常用模块配置以及模块中所支持的功能。

  • 消费端:XBAC模型:基于角色的RBAC(Role)、基于属性的ABAC(Attribute)、基于策略的PBAC(Policy)。

  • 认证/授权流程:再谈Zero中的 @Wall、详解 401/403 流程。

  • 管理端:可扩展面板式架构,现阶段版本支持 菜单管理、流程定制 两类,您可以扩展自己所需的不同权限管理的结构。

Zero在线教程中讲解过可插拔模式的认证组件,此处略过认证部分(@Wall ),本书主要讲解扩展模块中的授权以及 zero-rbac 模块如何实现企业级需求的复杂授权流程,其中标准化认证采用了 OAuth 规范。

若想要启用Zero Extension中的安全管理,可在您的Maven中追加如下依赖:

    <!-- RBAC专用模块 -->
    <dependency>
        <groupId>cn.vertxup</groupId>
        <artifactId>zero-rbac</artifactId>
        <version>ZERO_VERSION</version> <!-- 版本跟随Zero版本 -->
    </dependency>

全局配置

模块配置

zero-rbac 模块的基本模块配置位于文件 plugin/rbac/configuration.json 中,下边是一个完整的范例

{
    "condition": {
        "user": [
            "sigma"
        ],
        "role": [
            "sigma"
        ],
        "group": [
            "sigma"
        ],
        "permission": [
            "sigma"
        ],
        "resource": [
            "sigma"
        ],
        "action": [
            "sigma"
        ]
    },
    "codeExpired": 30,
    "codeLength": 6,
    "tokenExpired": 30,
    "poolCode": "POOL_CODE",
    "poolToken": "POOL_TOKEN",
    "poolPermission": "POOL_PERMISSIONS",
    "poolResource": "POOL_RESOURCES",
    "supportMultiApp": true,
    "supportIntegration": false,
    "supportGroup": true,
    "supportSecondary": true,
    "verifyCode": true,
    "verifyLimitation": 3,
    "category": {
        "employee": {
            "classDao": "cn.vertxup.erp.domain.tables.daos.EEmployeeDao",
            "condition": {
                "workNumber,!n": ""
            },
            "mapping": {
                "modelKey": "employeeId"
            }
        }
    },
    "initializePassword": "B59C67BF196A4758191E42F76670CEBA",
    "initialize": {
        "scope": "zero.app.demo",
        "grantType": "authorization_code"
    }
}

上述配置是一个完整的DEMO,除了初始化密码部分为了脱敏修改过,其他内容都是可参考的:

属性 含义

condition

默认选择对应实体的查询条件,多应用模式下可直接支持。

codeExpired

临时授权码的时间长度,默认 30s。

codeLength

临时授权码的字符串长度,默认 6位。

tokenExpired

令牌过期时间,默认 30分钟(由于目前版本使用了持久化令牌,该功能还未完善)。

poolCode

临时授权码的缓存池名称。

poolToken

令牌缓存池名称。

poolPermission

权限缓存池名称。

poolResource

资源缓存池名称。

supportMultiApp

是否支持多应用模式。

supportIntegration

是否支持和第三方应用的安全集成。

supportGroup

是否支持用户组功能。

supportSecondary

是否支持权限的二级缓存功能。

verifyCode

是否支持验证码。

verifyLimitation

设置用户登录限制,密码错误超过该值账户会被默认锁定。

initializePassword

新建账号时的初始化密码(一般是导入过程中使用)。

initialize

初始化账号时所需的基本账号信息。

category

打开异构账号模式。

现阶段版本中 token 默认是8小时时间,即上班时间生效,所以令牌过期增强功能目前不支持,正式上线之前会逐步完善功能,目前是缺失的(令牌过期参数 tokenExpired 无效)。

新版本之后您可以只使用如下配置:

{
    "supportGroup": true,
    "supportSecondary": true,
    "verifyCode": true,
    "verifyLimitation": 3,
    "category": {
        "employee": {
            "classDao": "cn.vertxup.erp.domain.tables.daos.EEmployeeDao",
            "condition": {
                "workNumber,!n": ""
            },
            "mapping": {
                "modelKey": "key"
            }
        }
    },
    "initializePassword": "B59C67BF196A4758191E42F76670CEBA",
    "initialize": {
        "scope": "zero.app.demo",
        "grantType": "authorization_code"
    }
}
关于异构账号模式

Zero权限框架中的 S_USER 有两个特殊字段:

属性 字段 含义

modelKey

MODEL_KEY

账号关联模型类型。

modelId

MODEL_ID

账号关联上述模型中的主键(ID)。

从配置 category 中可以了解和账号连接的相关配置,示例配置中的值对应:

  • modelKey = employee

  • modelId = key(员工表 E_EMPLOYEE 中的主键)

0

消费端

XBAC模型

Zero中的基本权限主体模型如下:

zarch perm

上图中的核心概念如下:

实体表 概念 含义

S_USER

用户

登录系统专用账号信息表。

S_ROLE

角色

和账号关联的角色信息表。

S_GROUP

用户组

和账号关联的用户组信息表。

S_PERMISSION

权限

和角色相关联的权限记录表。

S_PERM_SET

权限集

服务于权限定义的权限集合表,主要用于管理端构造权限集合实现批量授权。

S_ACTION

操作

隶属于权限记录的操作集合,操作最终会绑定到对应的操作主体上。

  • 所有关联表(R_ 前缀)目前都是多对多结构,一个用户可关联多个角色,一个用户可关联多个用户组,一个用户组关联多个角色,一个角色关联多个权限记录,一个权限记录包含多个 安全操作

  • 用户组支持继承结构,包括父组和子组的概念,权限计算过程中,不同算法在父子级结构中发挥的作用会有所区别。

  • 角色和用户组关联过程中支持优先级,即用户在关联角色和用户组时存在 第一角色、第一用户组 的概念。

常用的基本配置中,只要资源所需操作级别和基础算法模型计算的最终权限路径和用户所拥有的权限路径是通的,那么就可以实现基础权限的认证,这部分内容在完成本章节所有讲解之后会有更深入的说明。

安全操作 S_ACTION 对端关联到资源表 S_RESOURCE 和资源形成1对1的绑定,资源中定义的就是访问资源的基本要求,而S_ACTION中计算出来的结果就是登录账号所拥有的资源访问资格,当资源访问资格满足了资源基本要求时就认证通过,证明当前账号有权限访问该资源。但是、但是、但是——此处只是解决了账号:能不能 的问题,在资源访问之后还会有两张专用的数据表来执行 访问多少 的问题,这是Zero中数据域的实现原理,在后续的 N维安全视图 中加以说明。

多态身份(Profile)

Zero中由于复杂的多对多结构,最终会形成不同的多态身份(Profile),S_RESOURCE 表中有如下字段对资源访问资格执行定义:

字段名 含义 取值

MODE_ROLE

按角色查找资源的模式

UNION、EAGER、LAZY、INTERSECT

MODE_GROUP

按用户组查找资源的模式

HORIZON、CRITICAL、OVERLOOK

MODE_TREE

用户组继承和非继承树模式查找

EXTEND、PARENT、CHILD、INHERIT

上述取值是资源对多态身份的定义,最终形成的多态身份 Profile 的值列表如下(举例定义角色和组的优先级):

  • 多态身份只有在多角色、关联用户组、多用户组模式下生效,如果只包含一个角色,多态身份会演变成最简单的 USER_UNION 模式。

  • 用户/角色、用户组/角色在计算时都支持 UNION, EAGER, LAZY, INTERSECT 四种模式。

  • 使用用户组模式时,只要计算出选择使用什么用户组,就可以执行用户组对应的角色关联权限集的计算。

不含组模式

假设用户包含如下信息,这种模式(高频使用模式)下 MODE_GROUP / MODE_TREE 都设置成 NULL

R1(H):P1、P2、P3,高优先级角色,包含三条权限记录。
R2(L):P2、P4,低优先级角色,包含两条权限记录。

zero p user

含义

USER_UNION

zero p user union 并集模式,最终权限集为 P1, P2, P3, P4

USER_EAGER

zero p user eager 高优先级模式,使用最高优先级角色的权限集,此处:P1, P2, P3

USER_LAZY

zero p user lazy 低优先级模式,使用最低优先级角色的权限集,此处:P2, P4

USER_INTERSECT

zero p user intersect 交集模式,最终权限集为 P2

用户组模式

假设用户包含如下信息:

用户组结构如下(此处不列举组所对应的权限集):
         G10
        /   \
      G20   G21
     /   \    \
    G30  G31  G32
而登录用户只包含如下三个用户组:
G20(H):高优先级用户组
G31(M):中优先级用户组
G32(L):低优先级用户组
全量优先级:
G10 > G20 > G21 > G30 > G31 > G32

zero p group

组计算 角色计算 计算流程

HORIZON_UNION

zero p group u

zero p user union

UNION模式计算单个用户组关联角色权限集,再将三个组的权限集合并。

HORIZON_EAGER

zero p group u

zero p user eager

EAGER模式计算单个用户组关联角色权限集,再将三个组的权限集合并。

HORIZON_LAZY

zero p group u

zero p user lazy

LAZY模式计算单个用户组关联角色权限集,再将三个组的权限集合并。

HORIZON_INTERSECT

zero p group u

zero p user intersect

INTERSECT模式计算单个用户组关联角色权限集,再将三个组的权限集合并。

CRITICAL_UNION

zero p group h

zero p user union

UNION模式计算高优先级组的权限集。

CRITICAL_EAGER

zero p group h

zero p user eager

EAGER模式计算高优先级组的权限集。

CRITICAL_LAZY

zero p group h

zero p user lazy

LAZY模式计算高优先级组的权限集。

CRITICAL_INTERSECT

zero p group h

zero p user intersect

INTERSECT模式计算高优先级的权限集。

OVERLOOK_UNION

zero p group l

zero p user union

UNION模式计算低优先级组的权限集。

OVERLOOK_EAGER

zero p group l

zero p user eager

EAGER模式计算低优先级组的权限集。

OVERLOOK_LAZY

zero p group l

zero p user lazy

LAZY模式计算低优先级组的权限集。

OVERLOOK_INTERSECT

zero p group l

zero p user intersect

INTERSECT模式计算低优先级组的权限集。

PARENT_HORIZON_UNION

zero p parent gu

zero p user union

先查找三个组的父组,再按UNION模式计算权限集。

PARENT_HORIZON_EAGER

zero p parent gu

zero p user eager

先查找三个组的父组,再按EAGER模式计算权限集。

PARENT_HORIZON_LAZY

zero p parent gu

zero p user lazy

先查找三个组的父组,再按LAZY模式计算权限集。

PARENT_HORIZON_INTERSECT

zero p parent gu

zero p user intersect

先查找三个组的父组,再按INTERSECT模式计算权限集。

PARENT_CRITICAL_UNION

zero p parent gh

zero p user union

先查找优先级最高组的父组,再按UNION模式计算权限集。

PARENT_CRITICAL_EAGER

zero p parent gh

zero p user eager

先查找优先级最高组的父组,再按EAGER模式计算权限集。

PARENT_CRITICAL_LAZY

zero p parent gh

zero p user lazy

先查找优先级最高组的父组,再按LAZY模式计算权限集。

PARENT_CRITICAL_INTERSECT

zero p parent gh

zero p user intersect

先查找优先级最高组的父组,再按INTERSECT模式计算权限集。

PARENT_OVERLOOK_UNION

zero p parent gl

zero p user union

先查找优先级最低组的父组,再按UNION模式计算权限集。

PARENT_OVERLOOK_EAGER

zero p parent gl

zero p user eager

先查找优先级最低组的父组,再按EAGER模式计算权限集。

PARENT_OVERLOOK_LAZY

zero p parent gl

zero p user lazy

先查找优先级最低组的父组,再按LAZY模式计算权限集。

PARENT_OVERLOOK_INTERSECT

zero p parent gl

zero p user intersect

先查找优先级最低组的父组,再按INTERSECT模式计算权限集。

CHILD_HORIZON_UNION

zero p child gu

zero p user union

先查找所有组的子组,再按UNION模式计算权限集。

CHILD_HORIZON_EAGER

zero p child gu

zero p user eager

先查找所有组的子组,再按EAGER模式计算权限集。

CHILD_HORIZON_LAZY

zero p child gu

zero p user lazy

先查找所有组的子组,再按LAZY模式计算权限集。

CHILD_HORIZON_INTERSECT

zero p child gu

zero p user intersect

先查找所有组的子组,再按INTERSECT模式计算权限集。

CHILD_CRITICAL_UNION

zero p child gu

zero p user union

先查找优先级最高组的子组,再按UNION模式计算权限集。

CHILD_CRITICAL_EAGER

zero p child gu

zero p user eager

先查找优先级最高组的子组,再按EAGER模式计算权限集。

CHILD_CRITICAL_LAZY

zero p child gu

zero p user lazy

先查找优先级最高组的子组,再按LAZY模式计算权限集。

CHILD_CRITICAL_INTERSECT

zero p child gu

zero p user intersect

先查找优先级最高组的子组,再按INTERSECT模式计算权限集。

CHILD_OVERLOOK_UNION

zero p child gl

zero p user union

(无权限)先查找优先级最低组的子组,再按UNION模式计算权限集。

CHILD_OVERLOOK_EAGER

zero p child gl

zero p user eager

(无权限)先查找优先级最低组的子组,再按EAGER模式计算权限集。

CHILD_OVERLOOK_LAZY

zero p child gl

zero p user lazy

(无权限)先查找优先级最低组的子组,再按LAZY模式计算权限集。

CHILD_OVERLOOK_INTERSECT

zero p child gl

zero p user intersect

(无权限)先查找优先级最低组的子组,再按INTERSECT模式计算权限集。

INHERIT_HORIZON_UNION

zero p inherit gu

zero p user union

先查找所有组父组包含本组,再按UNION模式计算权限集。

INHERIT_HORIZON_EAGER

zero p inherit gu

zero p user eager

先查找所有组父组包含本组,再按EAGER模式计算权限集。

INHERIT_HORIZON_LAZY

zero p inherit gu

zero p user lazy

先查找所有组父组包含本组,再按LAZY模式计算权限集。

INHERIT_HORIZON_INTERSECT

zero p inherit gu

zero p user intersect

先查找所有组父组包含本组,再按INTERSECT模式计算权限集。

INHERIT_CRITICAL_UNION

zero p inherit gh

zero p user union

先查找优先级高组的父组包含本组,再按UNION模式计算权限集。

INHERIT_CRITICAL_EAGER

zero p inherit gh

zero p user eager

先查找优先级高组的父组包含本组,再按EAGER模式计算权限集。

INHERIT_CRITICAL_LAZY

zero p inherit gh

zero p user lazy

先查找优先级高组的父组包含本组,再按LAZY模式计算权限集。

INHERIT_CRITICAL_INTERSECT

zero p inherit gh

zero p user intersect

先查找优先级高组的父组包含本组,再按INTERSECT模式计算权限集。

INHERIT_OVERLOOK_UNION

zero p inherit gl

zero p user union

先查找优先级低组的父组包含本组,再按UNION模式计算权限集。

INHERIT_OVERLOOK_EAGER

zero p inherit gl

zero p user eager

先查找优先级低组的父组包含本组,再按EAGER模式计算权限集。

INHERIT_OVERLOOK_LAZY

zero p inherit gl

zero p user lazy

先查找优先级低组的父组包含本组,再按LAZY模式计算权限集。

INHERIT_OVERLOOK_INTERSECT

zero p inherit gl

zero p user intersect

先查找优先级低组的父组包含本组,再按INTERSECT模式计算权限集。

EXTEND_HORIZON_UNION

zero p extend gu

zero p user union

先查找所有组子组包含本组,再按UNION模式计算权限集。

EXTEND_HORIZON_EAGER

zero p extend gu

zero p user eager

先查找所有组子组包含本组,再按EAGER模式计算权限集。

EXTEND_HORIZON_LAZY

zero p extend gu

zero p user lazy

先查找所有组子组包含本组,再按LAZY模式计算权限集。

EXTEND_HORIZON_INTERSECT

zero p extend gu

zero p user intersect

先查找所有组子组包含本组,再按INTERSECT模式计算权限集。

EXTEND_CRITICAL_UNION

zero p extend gh

zero p user union

先查找优先级高组的子组包含本组,再按UNION模式计算权限集。

EXTEND_CRITICAL_EAGER

zero p extend gh

zero p user eager

先查找优先级高组的子组包含本组,再按EAGER模式计算权限集。

EXTEND_CRITICAL_LAZY

zero p extend gh

zero p user lazy

先查找优先级高组的子组包含本组,再按LAZY模式计算权限集。

EXTEND_CRITICAL_INTERSECT

zero p extend gh

zero p user intersect

先查找优先级高组的子组包含本组,再按INTERSECT模式计算权限集。

EXTEND_OVERLOOK_UNION

zero p extend gl

zero p user union

先查找优先级低组的子组包含本组,再按UNION模式计算权限集。

EXTEND_OVERLOOK_EAGER

zero p extend gl

zero p user eager

先查找优先级低组的子组包含本组,再按EAGER模式计算权限集。

EXTEND_OVERLOOK_LAZY

zero p extend gl

zero p user lazy

先查找优先级低组的子组包含本组,再按LAZY模式计算权限集。

EXTEND_OVERLOOK_INTERSECT

zero p extend gl

zero p user intersect

先查找优先级低组的子组包含本组,再按INTERSECT模式计算权限集。

多态身份Profile是整个 Zero权限框架中的一个 过度设计 的典范,从实际场景看起来真正使用到这部分的内容仅局限于 USER_X 四种计算模式。但根据设计时的调研和考察,有这部分功能之后,对于复杂组织架构之间的权限控制可以达到非常细粒度的级别,并且在变化过程中,可实现更多变化模式下的权限控制。

对用户而言,一旦登录之后,自己的 Profile 就已经固定,而资源需求要求的Profile则不一定固定,属于变量,最终计算结果近似于查找最短路径,达到用户组这个级别的额外变化模式(包括继承、派生、限制、组合等),最终 Zero权限框架中合计支持64种Profile配置,如此就解决了资源 能不能 访问的问题。

N维安全视图(View)

前文解决了资源 能不能 访问的问题,本章就在可访问的基础上解决 访问多少 的问题,Zero权限框架中的 S_RESOURCES_ACTION 是强绑定关系,它们之间只会单纯对比操作级别和资源需求级别是否可访问,一旦访问成功,就会衍生计算读写操作的边界,在Zero权限框架中读写边界的划定取决于安全视图 S_VIEW 中的定义。

安全视图基础

安全视图的基础维度如下:

字段名 含义 取值

NAME

视图名称

默认取值 DEFAULT。

POSITION

视图位置

默认取值 DEFAULT。

OWNER_TYPE

视图所属者类型

只包含两种:ROLE-角色视图,USER-用户视图。

OWNER

视图所属者ID

如果是角色视图则是角色ID,如果是用户视图则是用户ID。

RESOURCE_ID

视图所属资源

当前视图关联的 S_RESOURCE 资源ID,一个资源可能存在多个安全视图。

POSITION 和 NAME 构造视图的核心维度,在系统出现不同需求时会起重要作用:

场景 NAME POSITION

单模块无视图管理

DEFAULT

DEFAULT

单模块带视图管理

?

DEFAULT

多模块无视图管理

DEFAULT

?

多模块多视图管理

?

?

此处解释一下模块的概念,此处的 模块 并非我们开发过程中的模块,此处的模块底层关联模型只有一个,而模块更多是从列表作为入口。例如:

  • 单纯的CRUD应用(角色管理),管理过程中它的模块只有一个,从角色列表进入,然后管理,一般场景不会开第二模块进行特殊管理。

  • 带分类的CRUD应用(订单管理),可能带有不同列表管理处理,如正在执行的订单模块、已经完成的订单模块,而此时后台关联的模型依旧是 订单,这种情况下 POSITION 就显得很重要。

而且 POSITION 会比资源多一个维度,通常资源是后接口绑定,如 /api/xxx/search 的资源接口,但这个资源接口由于支持查询引擎语法,可能应用于不同的菜单入口(上述提到的正在执行的订单、已完成的订单)等,这种情况下两个菜单共享了一个资源,而为了针对不同的菜单定义 角色视图/用户视图,最好的方式就是启用 POSITION 参数。如此计算下来,POSITION既不和查询条件绑定(不同页面、不同位置、同一查询条件),也不可以和页面绑定,如果出现 TAB 页签会造成同一个页面中出现两种不同的查询(可能是两种不同的 POSITION),最终它只能和列表的配置绑定,直接在前端中提供它的配置来完成和列表绑定的过程,这一块的用法属于 Zero权限框架中的难点,其应用范围十分广泛,现阶段通常使用场景如下:

  • 按类型划分位置信息:分类字段管理 /ambient/tabular/:type 不同页面取不同的 POSITION,实现抽象态的列表管理,这种思路同样适用于:档案、合同、项目、员工、客户、分类等。

  • 按状态划分位置信息:如待办列表和已办列表,最终访问资源可能都是 /api/todo/search,而由于状态不同,所以设置不同的 POSITION 实现视图的定制。

  • 按流程划分QBE:目前系统中流程右上角的QBE列表页是基于此种逻辑,几乎不使用开发的模式就定制完成。

参考下图的结构:

zero p view

上图结构可以看到 POSITIONNAME(在模块访问中通常使用 VIEW)的使用场景会有所差异:

  • POSITION主要用于模块维度的拓展,它的起点是模块。

  • VIEW主要用于视图拥有者维度的拓展,它的起点是拥有者,如角色、用户、目录级(实验版本)。

安全视图类

Zero中存在一个特殊的参数对象:

    public class Vis extends JsonObject

    // 该参数对象使用时可如下:
    @POST
    @Path("/{actor}/search")
    @Address(Addr.Post.SEARCH)
    @Adjust(Orders.MODULE)
    JsonObject search(@PathParam("actor") String actor,
                      @BodyParam JsonObject data,
                      @QueryParam(KName.MODULE) String module,
                      @PointParam(KName.VIEW) Vis view);

该参数的格式比较特殊,通常使用的是 [view,position] 的数据格式,也是此处 @PointParam 注解解析的内容,它可以将上述格式直接解析成视图的两个核心维度( NAME, POSITION ),并将该维度应用于任意支持它的接口。Vis类中存在一个特殊方法smart会对视图数据格式做智能解析,它支持的几种格式如下:

  • Vis类型:如果传入的类型是Vis类型,则直接做引用赋值。

  • JsonObject类型:如果传入的类型是JsonObject类型,则解析格式:{"position": "xxx","view":"xxx"}

  • JsonArray类型:如果传入的类型是JsonArray类型,则解析格式:[view,position]

  • String类型:如果传入类型是String类型,除了完成URL的 decode 流程之外:

    • 如果String类型是JsonObject格式则做一次强制转换,执行JsonObject类型解析。

    • 如果String类型是JsonArray格式则做一次强制转换,执行JsonArray类型解析。

    • 否则String类型参数直接作为视图名称看待,而赋 POSITION 为默认值 DEFAULT

  • 默认创建专用默认视图:view = DEFAULT, position = DEFAULT

窗口定义

安全视图的窗口定义主要依靠下边几个字段:

字段名 含义

PROJECTION

JsonArray格式,执行该视图的列过滤,直接过滤掉接口返回数据的列信息。

CRITERIA

JsonObject格式,执行该视图默认的 Qr 语法注入,启用查询引擎追加接口的查询条件语法。

ROWS

JsonObject格式,针对行数据执行筛选,生成 IN 语句筛选特定行,通常是查询引擎无法做细粒度筛选时的一种折中选择。

VISITANT

布尔值,是否启用 虚拟视图(资源访问者)

  • PROJECTION 会作用于不同类型的前端组件,通常用于 LIST/FORM 两种,Zero框架中保存列表的列信息以及表单中针对部分表单执行字段过滤就依赖它来完成,它是后置过滤(实际会从数据库中查询出所有信息进行值提取,现阶段没有明显的性能问题)。

  • CRITERIA 主要针对于查询,它会隐式修改查询引擎的 Qr 语法,导致前端发送查询条件在安全视图作用下被直接修改,如果用户中出现了多个角色、多个用户组,则按照最终资源需求中定义的 Profile 来完成查询条件的拼合,默认模式下多个角色之间使用 OR 连接符。

  • ROWS 针对特殊资源提取,提供基于主键的直接命中条件,解决异构查询模式下用户无法使用Join的情况,由于表单是单条数据记录,所以一般表单接口无法支持该属性(设置了也没有效果);通常此属性作用于列表:

    • DATA:在数据层面,列表处理过程中直接针对条件执行过滤,典型应用为:菜单筛选、字典筛选、分类树筛选。

    • META:元数据层面,处理过程中只读取ROWS中设置过的的信息(特殊模式下界面呈现模式出现ReadOnly时,它的定义位于UI配置中,而不是安全视图层。

虚拟视图(资源访问者)部分参考下一个章节的详细说明,上述限制中,虽然 ROWS 无法直接作用于表单属性级的过滤窗口操作,但是它可以实现动态表单(/api/ui/form)模式下的属性集过滤,比如读取表单的属性有25个,使用 `ROWS`直接过滤掉一部分留下一部分,最终输出的是配置、配置、配置,最终表单引擎根据配置渲染表单,可以完成间接作用流程。

视图检索流程

看完了上述安全视图的方方面面之后,视图检索流程就变得很简单了(后端会根据访问资源键值生成 session-<METHOD>:<URI>:<POSITION>/<VIEW> 格式的视图缓存键)。下边是用户访问某个资源接口时的详细流程:

  1. 用户发送请求到某个资源接口如:/api/xxx/resource

  2. 系统检索该资源是否存在用户级的 S_VIEW 记录(OWNER_TYPE = USER, OWNER = <USER_ID>),如果存在该记录,则直接提取安全视图记录对资源执行前后(BEFORE/AFTER)计算。

    • BEFORE计算:BEFORE计算的核心算法主要在于修改参数,它的性能更高,一般直接作用于 criteria 参数部分实现查询条件的注入流程。

    • AFTER计算:AFTER计算会完整访问数据库,在查询出来的结果集中做运算,虽然性能会有损耗,但在某些场景下(需要全量元数据)是必须要走AFTER的。

  3. 若不存在用户级的 S_VIEW 记录,则继续检索是否有角色级的 S_VIEW 记录,若存在则计算。

  4. 上述两步都不存在时,忽略安全视图,可访问资源内所有内容。

从上述流程可以知道,用户级安全视图优先级比角色高,一般用户级安全视图都是个人视图模式存在,比如某个模块的视图管理,而角色级的视图都是管理员预设,单个用户不可以更改,比如管理员直接针对财务人员以外的角色设置不可访问某些资源的固定列如薪资、账期等。

最终 访问多少 的问题就直接被安全视图处理掉了,不同角色不同用户在此框架之下访问同一个接口时返回数据就可能出现不同,那么这样就解决了资源重用并且 访问多少 的问题。

资源访问者(Visitor)

多用于抽象层次比较高的 动态建模 领域的权限控制。

本章进入Zero权限框架中一个新的领域:虚拟资源/资源访问者,在讲虚拟资源之前先思考:为什么要使用虚拟资源?前文安全视图中的定义不知道读者是否发现一个小问题:静态的——一旦绑定了资源之后,就只能在某个资源中直接设置参数和条件,而这里设置的所有条件以及参数都 不依赖输入,您考虑下边一个场景。

我在后端书写了一个接口:/api/xxx/:type,这个接口在后端的定义接口和 S_RESOURCE 记录仅有一条(为什么,除非您直接愿意写成 /api/xxx/type1/api/xxx/type2 两个接口),这种场景下,意味着如果 type 有三个值,那么我所期望的安全视图应该有三种,根据前文提到的,您可以设置 S_VIEWPOSITION 来限定资源视图,就完成了三种模式下的定义。但若现在我的 :type 参数不是三种,可能存在N种或者上百种,如何解决?Zero为了解决这种安全视图方案,提供了 资源访问者 的概念。资源访问者存储于后端配置表 S_VISITANT 中,一个 S_VIEW(严格说是 VISITANT = true 的视图记录)可能包含无数个资源访问者。

zero p visit arch

从上图可以知道在动态建模过程中,路由器防火墙 可能意味着不同模型,而且应用程序会不断添加新的设备类型,产生新的模型,这种模式下使用 POSITION 扩展会造成大量冗余,况且此时的入口是虚拟资源,即意味着:您在访问该资源时,您是不知道您访问的资源究竟是路由器还是防火墙,那么这样的流程中,就只能通过 访问者 语法计算出真实访问者,然后根据资源访问者去定位真实的资源。从这点意义上讲您可以理解两点:

  • POSITION:先知模式,在访问资源之前,您已经知道您要访问的维度数据是什么,资源访问入口是具象层。

  • VISITANT:后知模式,在访问资源之前,您不知道您要访问的维度数据是什么,资源访问入口是抽象层(这也是虚拟资源一词的来源)。

Zero中按照如下流程配置一个资源访问者:

  1. 资源本身(S_RESOURCE)将该资源定义成一个虚拟资源(VIRTUAL = true)。

  2. 在(S_RESOURCE)资源字段中定义访问者基本规则:

    • 设置访问者语法 SEEK_SYNTAX 字段。

    • 设置访问者配置 SEEK_CONFIG 字段。

    • 设置访问者组件 SEEK_COMPONENT 字段(Java类名)。

  3. 用户发送请求过来时会读取 S_VIEW 的视图信息,一旦读取到视图信息后,对 VISITANT = true执行校验,校验成功之后执行资源访问者流程。

资源访问者流程可以总结成两个大步骤:

步骤 说明

真实资源查找

根据 S_RESOURCEVIRTUAL = true 的定义去查找真实资源,真实资源的查找就借用访问者配置(前文提到的基本规则)来实现。

访问者安全视图

根据 S_VIEWVISITANT = true 的定义去同步查找真实资源模式下的安全视图,该视图比 S_VIEW 中扩展的属性更多,渗透到表单级、操作级、非接口级,数据更强大的ACL控制视图逻辑。

访问者逻辑

资源访问者的内部逻辑流程如下:

  1. 检查访问视图 S_VIEW 是否一个带有访问者的安全视图(VISITANT = true)的定义。

  2. 根据资源 S_RESOURCE 中的访问者定义(seekSyntax / seekConfig / seekComponent)计算真实资源访问规则。

  3. 扫描资源访问者的 模式(Replace/Extend)和 作用阶段(EAGER/DELAY),并执行访问者操作。

  4. 读取访问者相关信息,将这些信息和 S_VIEW 中的信息合并计算,计算访问者安全窗口。

关于访问者语法结构定义在此多做点说明,在实际场景中,通常资源的抽象模式(访问者维度)都只有一个维度,所以访问者语法也只是针对资源的访问做一个维度层面的拓展,若您的应用中真的出现了 超级接口,访问者维度需要通过多维模式去实现时,您可以自定义扩展:

  • 单维度标准化:seekSyntax 定义可完成。

  • 多维度标准化:seekSyntax 新版多维访问者定义完成。

  • 多维度扩展:seekConfig/seekComponent 自定义完成。

个人不推荐在系统中做过多的 超级接口 ——一个接口完成多种不同的逻辑,这样的方式运维将会造成一定的压力,并且不容易拓展,但在动态建模和动态接口部分,往往会牵涉到模型的变体,变体出现时这种模式的维度是必须存在的,所以就只能依赖扩展配置来完成。

多说一句:Zero扩展框架中存在很多 config/component 的架构,通常 component 是一个实现了固定接口的Java类,而 config 则是和该类配套的配置数据(Json格式),这样的格式可以让任何开发人员自由发挥拓展自己的应用,访问者逻辑部分也是如此。

访问者语法 存在的目的是查找视图中对应的访问者信息,由于一个视图可能存在多个访问者,所以系统必须保证本次请求的访问者信息——要么只有1个,要么不存在;否则系统会出现二义性问题导致最终无法定位使用哪个资源访问者来处理请求,所以目前Zero权限框架的版本中,访问者主键 S_VISITANT 表中的 SEEK_KEY 字段是唯一的,而访问者语法的最终目的就是计算 SEEK_KEY 生成访问者查询条件。

(执行维度)模式和阶段

资源访问者语法中的模式 mode 信息( S_VISITANT 表中的 MODE 字段):

  • Replace:替换模式,这种模式下,访问者视图会直接覆盖 S_VIEW 中的安全窗口规则,也就是说资源访问不再遵循 S_VIEW 中的安全规则,而直接使用访问者规则。

  • Extend:扩展模式,这种模式下,访问者视图会和 S_VIEW 中的安全窗口规则合并计算,形成新的组合好的访问者规则。

它的作用如下:

zero p visit mode

资源访问者语法中的阶段 phase 信息( S_VISITANT 表中的 PHASE 字段):

  • EAGER:通常当前资源立即生效,一般执行数据读取时会使用 EAGER 阶段(就在当前接口生效)。

  • DELAY:这种阶段通常是读取配置项作用于子资源或其他资源时生效,一般读取元数据和配置数据时使用 DELAY 阶段(DELAY阶段 DataRegion 中的视图模式依旧生效)。

它的作用如下:

zero p visit phase

对于直接开发的接口,基本上EAGER模式就可以满足大量的接口安全控制需求,您不需要开发额外的逻辑就实现了安全视图的控制,但对于比较特殊的基于配置的流程中,通常接口会分为:元数据接口和数据接口两部分,二者相互影响并相互作用,这样的条件下,资源访问者就体现出它的价值了,一个访问者就解决了元数据接口和数据接口的双重安全作用,而不需要依赖每个位置都定义对应的接口。

(定义维度)类型/标识/唯一键

访问者记录中虽然保存了 SEEK_KEY,但在不同的业务场景中,它无法做 全局标识,所以根据实际需求,此处设计在后期做过一些基本改动,访问者标识维度如下:

字段 含义

TYPE

访问者类型,针对资源维度的分类定义,现阶段支持:FORM、LIST、OP、VIEW 四种,服务于 zero-atom 动态建模。

IDENTIFIER

模型标识符,针对模型维度的分类定义,主要提供master模型的基础访问者(限定于所有场景)。

SEEK_KEY

访问者主键,系统可以根据访问者主键查询访问者信息。

访问者标识维度从执行和定义两个方向处理:

  1. 定义维度:VIEW_ID, TYPE, IDENTIFIER 形成唯一键。

  2. 执行维度:VIEW_ID, TYPE, SEEK_KEY 形成唯一键。

举个例子,在读取表单配置时,表单的配置可直接使用:VIEW_ID,TYPE,SEEK_KEY 限定:

  • VIEW_ID :标识了读取表单的资源接口信息。

  • TYPE :手动定义,静态模式下使用固定值,动态模式中则可以直接使用限定值。

  • SEEK_KEY :根据表单计算这种类型的主键,此时它的值可以是表单的ID(动态),也可以是表单的CODE(静态)。

这样处理之后就实现了表单资源的唯一访问者读取。

SEEK_KEY 目前最常用的两种场景如:

  1. 动态建模过程中,资源访问会牵涉 controlId(表单、列表的组件ID),它的格式通常如下:

    {
        "type": "LIST / FORM / OP",
        "controlId": "UI_CONTROL表中定义的组件的ID,由于UI_CONTROL中已经定义了模型标识符,所以此处模型标识符则可直接省略。"
    }
  2. 静态建模过程中,资源访问者会牵涉到其他内容来构造 SEEK_KEY,如:

    {
        "type": "LIST / FORM /OP",
        "workflow": "工作流名称"
    }

现在版本中流程 SEEK_KEY 的值格式通常是::workflow/:event/DEFAULT ,其中包含了流程名称和任务名称,这样就可以保证流程中的每个节点都可设置访问者,配合 TYPE 字段实现列表、表单、操作不同级别的访问。

访问控制

访问者对列表的控制基本和 S_VIEW 同源,主要覆盖如下:

字段 含义

DM_ROW

等价于视图中的 ROWS 限定规则,前端标记:H - Horizon,水平限定。

DM_QR

等价于视图中的 CRITERIA 限定规则,前端标记:Q - Query,查询限定。

DM_COLUMN

等价于视图中的 PROJECTION 限定规则,前端标记:V - Vertical,垂直限定。

访问者优于视图的地方在于它还提供了表单级的控制,主要覆盖如下:

字段 关键字 含义

ACL_VISIBLE

可见性

可见属性集设置,限定表单中的某些表单字段是否可见。

ACL_VIEW

只读

只读属性集设置,可编辑依靠计算:可见性 - 只读

*ACL_VARIETY

多样性

多样性属性集设置,用于控制集合类型的属性变体,如数组类的多样性属性集,递归三种属性集,标记某个子属性配置。

ACL_VOW

引用

针对引用类型的属性进行限定,比如订单中关联了员工信息,而员工信息可直接被订单接口的访问者执行表单级属性过滤。

ACL_VERGE

依赖

保存了所有依赖属性的相关信息,和引用方向相反。

上述属性中 ACL_VARIETY 属于特殊访问属性,通常可针对列表、集合等不同的访问属性进行内部数据结构的限定,有了这些限定之后,不同的用户在访问同一张表单可达到如下权限控制效果:

  • 访问的表单长相不同(可见性处理)。

  • 访问同样的表单部分属性看见的内容不同,包括只读、可编辑。

  • 表单引入:有值不可看的情况,这种情况下表单中会显示 ** 来处理(仅针对特殊角色和用户生效)。

  • 依赖属性的不可见处理,所有和表单关联的数据出现不可看的情况。

配置步骤

到这里,所有和Zero权限框架相关的内容就已经讲完了,本章节主要讲解配置步骤,开发人员可参考此章节配置上述所有内容。

权限基础配置

权限基础配置在于配置用户是否有权限访问某个资源。

  1. 根据:S_USER / S_ROLE / S_GROUP / S_PERMISSION 设置账号到权限的路径数据信息。

  2. 开发自定义接口如:/api/xxx/order,并且将此资源接口信息录入到 S_RESOURCE 表和 S_ACTION 表中,并将 S_ACTION 记录挂在某个 S_PERMISSION 权限之下。

    • S_RESOURCE 中针对Profile进行资源需求配置:MODE_ROLE / MODE_GROUP / MODE_TREE,确保用户最终计算出来的权限集在对应的Profile之中(参考:仅配置 MODE_ROLE = UNION)。

    • S_ACTION 中配置的 LEVEL 一定要大于 S_RESOURCE 中定义的操作级别。

  3. 将上述配置通过 Loader 导入系统中,重启容器生效(后续使用管理端则不需要重启)。

操作级别的内部定义:

符号 含义

0

READONLY

只读级别,最小等级。

1

READ

标准读取,通常是单记录读取。

2

READ_INTEGRATION

集成读取,通常会开放接口读取。

3

READ_EXPORT

从数据库中批量导出时所需。

4

ADD

创建和批量创建级别。

5

ADD_IMPORT

导入专用级别。

6

ADD_INTEGRATION

集成创建,通常会开放接口创建。

7

ADD_META

元数据创建接口,创建配置专用。

8

EDIT_APPROVE

审批确认更新接口,部分字段更新。

9

EDIT

标准编辑,更新和批量更新级别。

10

EDIT_INTEGRATION

集成更新,通常开放接口实现更新逻辑。

11

EDIT_META

元数据更新接口,更新配置专用。

12

DELETE

标准删除,删除和批量删除级别。

13

DELETE_PURGE

清除专用,通常用于数据清空。

14

DELETE_META

元数据删除专用,删除配置专用。

15

FULL

最高操作级别。

视图配置

一般静态视图配置都是配置角色级别的视图相关信息,通常不会配置用户级视图。

  1. 找到权限基础配置中 S_RESOURCE 的主键,填充到 S_VIEW 中的 RESOURCE_ID 字段中。

  2. 没有特殊说明 NAMEPOSITION 都可以直接配置成 DEFAULT 默认值。

  3. OWNER_TYPE 中配置 ROLE,然后找到登录账号对应的角色ID,填充到 OWNER 中。

  4. 配置视图的三个核心维度:ROWS, PROJECTION, CRITERIA

  5. 将上述配置的 S_VIEW 记录导入到系统中,重启容器生效(后续使用管理端则不需要重启)。

访问者配置

如果牵涉到访问者配置,则遵循如下流程执行:

  1. 将资源定义 S_RESOURCE 中的资源设置成 虚拟资源VIRTUAL = true)。

  2. 为虚拟资源配置 访问者语法,两种配置模式:

    • 直接配置 SEEK_SYNTAX 字段生成标准化的访问者语法模块。

    • 配置 SEEK_CONFIG / SEEK_COMPONENT 字段生成扩展的访问者语法模块。

  3. 配置好之后再根据所需为资源配置访问者信息:S_VISITANT 表中追加相关记录。

示例/格式

S_VIEW 中的 PROJECTION

S_VIEW 表结构中的 PROJECTION 通常是 JsonArray 结构,如:

[
    "key",
    "name",
    "code",
    "createdAt",
    "createdBy"
]

上述配置会包含两个含义:

  • 在读取资源过程中使用了该视图之后,只会读取 五列 信息。

  • 读取的 五列 会按照配置顺序读取,即列表中的列顺序为:key, name, code, createdAt, createdBy

S_VIEW 中的 CRITERIA

S_VIEW 表结构中的 CRITERIA 通常是 JsonObject 结构,直接追加查询引擎语法,最终条件和请求条件按 AND 操作符合并。

S_VIEW 中的 ROWS

S_ROWS 表结构中的 ROWS 是一个 JsonObject 结构,会生成 IN 查询条件,如:

{
    "name" : [ "zero.desktop", "zero.desktop.my", "zero.desktop.my.todo-pending"],
    "code" : [ "A", "B" ]
}

您可以将上述配置片段理解成 QR 查询引擎的片段语法,如上述代码中如果 name, code 对应字段为 NAME, CODE,则会生成查询条件:

WHERE NAME IN ('zero.destkop', 'zero.desktop.my', 'zero.desktop.my.todo-pending') OR CODE IN ('A', 'B')

根据 QR 查询引擎规范,您可以按下边配置将中间连接符配置成 AND

{
    "name": ["..."],
    "code": ["..."],
    "": true
}
S_RESOURCE 中的 SEEK_SYNTAX

SEEK_SYNTAX 通常格式如下:

{
    "phase": "AFTER",
    "data": {
        "type": "RECORD",
        "viewId": "`${viewId}`",
        "identifier": "psi.pos"
    }
}

上述代码是一个单维度访问者代码,data 节点会包含一个 type 属性,它和 phase 会构成不同的值处理模式,此处若您的数据来自于输入数据,则可使用 "`" 符号执行 JEXL 转换,如上述数据就会根据 type, viewId, identifier 计算访问者的 SEEK_KEY 来提取访问者信息。

上述配置中的 phasedata 节点中的 type 最终会生成如下格式的合法值:

phase type 含义

BEFORE_PROJECTION

BEFORE

PROJECTION

前置操作,修改查询引擎参数 projection。

BEFORE_CRITERIA

BEFORE

CRITERIA

前置操作,修改查询引擎参数 criteria。

AFTER_RECORD

AFTER

RECORD

后置操作,执行记录处理,单数据增删改的记录过滤访问者计算。

AFTER_ROWS

AFTER

ROWS

后置操作,多记录,执行行筛选,筛选出集合数据中的某些行数据访问者计算。

AFTER_COLLECTION

AFTER

COLLECTION

后置操作,多记录,执行列筛选,过滤部分属性。

SEEK_SYNTAX 的多维格式如下:

{
    "phase": "AFTER",
    "selector": "io.horizon.spi.secure.ConfineKind",
    "kind": "type",
    "data": {
        "ATOM": "`${identifier}/${control}`",
        "FLOW": "`${workflow}/${node}/DEFAULT`"
    }
}

上述代码段中,访问者调用中会使用 io.horizon.spi.secure.ConfineKind 完成访问者选择,选择过程根据输入参数 type 值执行计算,由于使用了组件,所以执行过程可忽略 data 节点的数据信息,最终:

  • type = ATOM 会生成访问者的 SEEK_KEY = <模型标识符>/<控件ID>,动态建模可用。

  • type = FLOW 会生成访问者的 SEEK_KEY = <工作流名称>/<节点名称>/DEFAULT,流程引擎专用。

您也可以定义自己的 Confine 接口组件配置 selector 完成多维数据访问者扩展。Confine接口的定义如下:

public interface Confine {
    Future<JsonObject> restrict(JsonObject request, JsonObject syntax);
}

其中此处的 syntax 就是 SEEK_SYNTAX 配置的内容,而 request 参数对应到前文中 data 节点可使用的预设参数(预设参数不包含您在 Http 请求体 Body 中传入的参数),预设参数如下:

参数名 默认值 含义

appId

多应用模式中,所属应用程序ID,来自请求头:X-App-Id

appKey

所属应用程序的Key,敏感数据访问专用,来自请求头:X-App-Key

sigma

统一标识符,来自请求头:X-Sigma

language

多语言模式中,当前应用的语言信息,来自请求头:X-Lang

tenantId

多租户模式中,所属租户的ID,来自请求头:X-Tenant-Id

resourceId

当前请求正在访问的资源信息。

viewId

当前资源访问者所属视图ID。

view

DEFAULT

当前资源访问者访问的视图名称,默认 DEFAULT

position

DEFAULT

当前资源访问者访问的视图位置,默认 DEFAULT

S_VISITANT 中的 ACL_

由于访问者表中的 DM_ 字段格式和 S_VIEW 中保持一致,此处不再重复讲解。

  • DM_ROW 对应 ROWS

  • DM_QR 对应 CRITERIA

  • DM_COLUMN 对应 PROJECTION

最后简单讲解下 ACL_ 表单级语法:

字段 类型 格式和含义

ACL_VERGE

JsonObject

一般格式是:field = JsonObject 结构,包含依赖字段名和依赖字段相关配置。

ACL_VISIBLE

JsonArray

直接是 [field1, field2, …​] 结构,包含了可见性字段集,最终会转换成 Set。

ACL_VIEW

JsonArray

直接是 [field1, field2, …​] 结构,包含了只读字段集,最终会转换成 Set。

ACL_VARIETY

JsonObject

Json格式,用于构造 AclView/AclMap专用,执行类型为 DATA。

ACL_VOW

JsonObject

Json格式,用于构造 AclView/AclMap专用,执行类型为 REFERENCE。

最后谈谈响应格式中的的 __acl 字段,这是Zero标准数据规范中的一部分,如果某个接口(包括JsonArray返回的接口)中包含了 __acl 则证明该接口包含ACL控制信息,它的格式如下:

{
    "access": [],
    "edition": [],
    "fields": []
}

上述格式是初期版本格式,代表:

  • fields:当前表单、列表能访问的所有属性集合。

  • access:您可以访问的所有属性集合。

  • edition:您可以编辑的所有属性集合。

认证流程

基本流程

Zero中的登录认证流程标准模式基于 OAuth 认证授权流,它的完整流程图如下:

zbac 401

认证流程主要走三步:

  1. 使用账号和密码( MD5 加密)访问标准登录接口 /oauth/login,拿到当前账号的 clientSecret 密钥。

  2. 使用账号ID和密钥访问临时授权码申请接口 /oauth/authorize,获取30s期限的临时授权码 code

  3. 使用临时授权码 code 访问令牌接口 /oauth/token,获取资源访问令牌。

  4. 将访问令牌放到 Authorization 请求头中获取资源信息,格式为 Bearer <access_token>

Zero权限框架基于 vertx-auth-common 项目自定义开发了,除了支持类似 OAuth 协议的认证授权流程以外,也支持其他的,为了统一相关应用,所以Zero Extension中采用了 OAuth 协议下的流程,框架本身支持 数字签名 功能,但由于目前应用安全性要求没那么高,所以暂时没有开启。

接口规范

本章讲解每个接口的相关细节,让您对接口细节有所了解,Zero框架做集成时这是必须步骤,基本的集成接口规范可参考 数据规范 章节的定义。

/oauth/login
登录接口

HTTP方法

POST

URI路径

/oauth/login

请求头

Content-Type: application/json

请求

{
    "username":"(账号)",
    "password":"(MD5加密过的密码)"
}
参数名 说明

username

用户账号。

password

账号密码(MD5加密并且转大写)。

响应

{
    "data": {
        "key": "返回的用户主键",
        "scope": "应用名称",
        "clientSecret": "用户专用密钥",
        "grantType": "authorization_code"
    }
}
参数名 说明

key

当前登录账号的 clientId。

scope

当前应用范围,链接到认证授权中的 realm 属性。

clienSecret

当前账号创建时生成的 clientSecret 密钥。

grantType

当前OAuth认证的方式,默认使用 authorization_code,后续可扩展。

/oauth/authorize
授权码接口

HTTP方法

POST

URI路径

/oauth/authorize

请求头

Content-Type: application/json

请求

{
    "client_id":"用户主键",
    "client_secret":"创建账号时生成的64位随机字符串,盐",
    "response_type":"...(保留)",
    "scope":"vie.app.ox"
}
参数名 说明

scope

必须和上一步的值一致,应用范围。

client_id

上一步返回的 clientId 值(登录才能拿到)。

client_secret

上一步返回的 clientSecret 值(登录才能拿到)。

response_type

上一步返回的 grantType 值(需一致)。

响应

{
    "data": {
        "code": "Gr0RzNkZ"
    }
}
参数名 说明

code

临时授权码,随机8位数,并且只有30秒的使用时限。

/oauth/token
令牌接口

HTTP方法

POST

URI路径

/oauth/token

请求头

Content-Type: application/json

请求

{
    "client_id":"用户的ID",
    "code":"Gr0RzNkZ"
}
参数名 说明

client_id

/oauth/login 接口中返回的 key

code

/oauth/authorize 中返回的临时授权码。

响应

{
    "data": {
        "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9....",
        "refresh_token": "eyJ0.....",
        "iat": 1578237840003
    }
}
参数名 说明

access_token

资源访问使用的 JWT 令牌。

refresh_token

资源访问令牌过期时需要重新生成令牌专用的刷新令牌。

iat

令牌过期时间

异常代码表

本章节末尾附上登录接口常见错误代码表

状态码 错误码 含义

500

-60007

服务器内部错误

400

-60004

请求格式不对,必须是Json对象格式

449

-80203

账号不存在

401

-80204

密码错误

401

-80202

scope参数丢失,clientId 和 clientSecret不匹配,无法生成 code

401

-80201

授权码code过期,无法生成 token

423

-80220

账号被锁定无法使用,联系管理员解锁

401

-80221

输入密码次数超过了系统限制,账号被临时锁定5分钟

401

-80222

验证码输入错误,重新输入

403

-80212

账号没有设置完成,无权登录当前应用

403

-80213

您对请求资源没有操作权限,访问终止,联系管理员授权

授权流程

前一章节提到了标准的登录流程,本章开始讲解 Zero Extension框架中的授权流程,授权流程基于第一章的消费端,同时还会包含和安全缓存相关的内容,本章意在将整个底层数据和原理级内容给您讲清楚,如此您就可以根据实际所需拓展开发相关功能了。

账号登录初始化

前文提到的三个接口调用,只能证明当前账号通过了系统的安全认证(401流程),登录成功之后,系统将会为登录账号初始化所有登录信息,本章详解初始化账号的系统级流程。

正常来说,Vert.x的Web应用中 vertx-web.session 是用来保存会话数据专用的会话键,但在之前某个版本使用过程中,由于配置错误导致我们自己的系统没有办法适配该会话,无法维持状态,最后引起的反作用就是太依赖它的会话机制进行编码,为了解决这种问题,最终 Zero 重新设计了一套会话管理模式(定义了 habitus 属性),既可以然兼容 vertx-web.session 信息,又可以自己直接从会话中提取状态数据和相关业务数据。

令牌生成流程

生成令牌过程中,根据 消费端 提到的数据结构,系统会为每个登录账号生成如下数据:

{
    "user": "xxx",
    "habitus": "xxx",
    "session": "xxx",
    "role": [],
    "group": []
}
属性 含义

user

用户主键(登录过程中使用的 clientId 值)

habitus

128位随机字符串,为了解决 401/403 过程中系统为会话账号重新生成 session 问题而量身打造的业务级 session key

session

从 Vert.x 中读取的会话ID,数据来自 vertx-web.session

role

数组结构、和当前账号绑定的角色信息

group

数组结构,且在 zero-rbac 中启用了用户组功能时会追加

获取了登录账号的上述信息之后,系统会提取 user, session, habitus 三个属性生成访问令牌 access_token(即访问令牌中只包含这三个属性信息),再根据访问令牌生成刷新令牌 refresh_token,以及根据配置计算出令牌过期时间,最终将访问令牌持久化到数据库中,持久化的目的是保证用户在异常管理浏览器或其他非正规操作时,依旧有一个时限内可使用的访问令牌。

登录会话初始化

令牌生成完成后,系统将会调用 ScUser 类中的静态方法初始化 用户登录缓存,ScUser类是Zero为账号封装过的核心用户类,此处不讲解该类的一些成员方法(如视图、Profile访问、用户主键提取),但下边几个高频方法可以给开发带来很大的福利。

方法签名 返回值 含义

user()

String

读取当前登录用户主键

user(String)

ScUser

设置当前登录用户主键

logout(String)

Future<Boolea>

将传入 habitus 会话的账号从系统注销

login(JsonObject)

Future<ScUser>

登录初始化,本章节的核心调用函数

login(String)

ScUser

使用 habitus 会话ID从环境中获取登录用户信息。

login(User)

ScUser

使用 Vert.x 中的安全对象 User 从环境中获取登录用户信息。

从安全性考虑,开发人员如果只有用户主键是无法从环境中拿到 ScUser 信息的,必须采用框架内置的方式从环境中获取 User 对象(User对象是被 Vert.x 安全框架托管的),使用该对象可直接获取到 ScUser 相关信息。

资源请求流程

用户发送请求时,直接将登录过程中拿到的 <access_token> 追加到资源访问的请求头中发送本次请求:

Authorization: Bearer <access_token>
流程图

参考下边的时序图看看整个资源请求流程的完整步骤(不包含安全视图和数据域部分):

0

关于缓存池

整个资源访问过程中会使用到两个缓存池用于优化认证授权性能,缓存本身带有 间接会话校验 以证明发送请求者的身份信息:

  1. ZERO-CACHE-401认证缓存池,存储用户身份、令牌、会话相关信息以验证请求识别身份。

  2. ZERO-CACHE-403授权缓存池,存储用户已经访问过的资源授权情况(可访问、不可访问),直接快速响应客户端用户 能不能 访问当前资源。

令牌组合

用户最开始登录拿到的 <access_token> 是访问令牌,其中数据包括:

{
    "user": "xxx",
    "session": "xxx",
    "habitus": "xxx"
}

在认证通过之后,存储到认证缓存池中的数据如下(后续授权流程、安全视图流程、资源访问者流程都会使用到令牌数据):

{
    "user": "xxx",
    "session": "xxx",
    "habitus": "xxx",
    "access_token": "xxx",
}

上边这种变化就是令牌组合干的事,组合令牌的目的是在特定插件、组件、签名等后端模块中实现快速比对,JWT Token的 encode/decode 过程从测试上看起来是十分费时的,如果每次需要使用令牌或者使用令牌中的数据都执行这个步骤会导致性能层面的损耗,所以此处是用空间换时间,令牌使用两种形态存储在缓存中,如何使用取决于最终的业务场景。

授权完整流程

前文中讲解了 资源请求流程 的完整步骤,由于认证流程相对简单(会话校验、令牌校验),而授权流程主要会包含三个核心步骤:

  1. 基本授权:用户是否有权访问该资源。

  2. 安全视图:当前资源是否启用了安全视图。

  3. 访问者:启用了安全视图的场景中是否执行访问者流程。

本章节主要针对基本授权流程细节进行讲解(其中 基本授权 属于资源请求流程的后半部分内容)。

标准化输入

认证流程通过后,授权最初会拿到如下输入数据信息(该数据同时也是调用 user.principal() 的结果):

{
    "user": "xxx",
    "session": "xxx",
    "habitus": "xxx",
    "access_token": "xxx",
}

该数据信息会在Zero安全框架内部执行标准化,标准化结果为:

{
    "user": "xxx",
    "session": "xxx",
    "habitus": "xxx",
    "access_token": "xxx",
    "metadata": {
        "method": "xxx",
        "uri": "xxx",
        "requestUri": "xxx",
        "view": {
            "view": "xxx",
            "position"; "xxx"
        }
    },
    "headers": {

    }
}

简单说明几点:

  1. 标准化过程追加了本次请求信息,使用该请求信息可直接提取 S_ACTION,而 S_ACTIONS_RESOURCE 是一对一绑定,等价于可直接提取资源信息。

  2. 视图存储采用了 Vis 对象(专用视图对象),包含了 viewposition 两个维度。

  3. headers 中主要捕捉自定义请求头,类似 X-Sigma, X-Lang 等这种。

还需要说明的一点就是注意 urirequestUri 的区别,一般在后端定义资源时会采用 uri,举个例子(按工号读取某个员工):

  • 后端定义GET接口:/api/employee/:workNumber,此处的路径就是 uri,后端查询 S_ACTION 以它为主。

  • 真正发送请求时:/api/employee/20230201,此处的路径就是 requestUri

两种不同的URI在不同的场景会分别参与不同后续流程运算。

基本授权步骤
  1. 使用标准化输入处理之后的数据构造 ScResource 对象(和 ScUser 类似是对资源结构的封装)。

  2. ScResource 中计算提取资源池中的资源键(resource-<method>:<uri>),使用该资源键访问 资源池

    • 如果资源池存在直接走安全视图流程。

    • 资源池中不存在则走步骤三。

  3. 根据请求数据( urimethod )从安全操作中提取 S_ACTION 记录锁定本次请求的安全操作。

  4. 根据安全操作提取绑定的资源记录,若资源所需操作等级大于了安全操作等级,证明资源出现了状况(比如升级、迁移等),安全操作失效。

  5. 操作检查结束后,根据提取的所有信息回写资源池,并完成和资源池的同步。

视图流程

资源同步完成后,就开始根据资源读取当前登录用户的视图信息,视图信息读取分两步:

  1. 用户视图(高优先级),如果存在用户视图则直接忽略角色视图。

  2. 角色视图,用户未做任何视图管理时资源的默认视图。

视图读取在消费端章节已经多次提及,此处就不赘述,用户视图实际是挂在用户登录的 habitus 缓存之下,视图键的计算如:

session-<method>:<uri>:<position>/<view>

视图在初始化时实际是针对资源池的 双读双写 计算,一方面确保资源池中的资源数据已经是经过了同步的数据,另外一方面保证当前视图绑定的资源信息具有强一致性。

  • 如果直接从用户的 habitus 缓存之下可以读取视图信息,则跳过初始化视图的流程。

  • 无法从缓存中提取视图信息时,则执行初始化流程(先用户、再角色)的提取模式。

资源访问者流程在数据域后期执行,下一章节将会详细介绍。前期执行该流程会导致访问者语法只能根据输入处理,无法根据存储数据处理导致访问者功能缺失,无法处理二阶段读写专用场景。

数据域 / 访问者

数据域功能在Zero框架中是使用 插件 方式启用的,若不启用数据域功能,那么视图功能将不复存在,所以插件位于 zero-rbac 项目中,属于Zero Extension框架部分的内容。数据域功能的开启一般在项目配置文件:vertx-dock.yml 中,配置片段如下:

# 「扩展组件」-----------------------------------------------------------------------
extension:
  region:
    component: io.vertx.mod.rbac.extension.DataRegion
    config:
      prefix: /api/
截断检查

数据域的功能要根据实际需求来指定,在Zero框架中它属于一个类似 Around 模式的AOP操作,众所周知AOP操作中如果执行代码过多有可能会引起性能问题,所以为了在需求和性能间找个折中点,系统根据配置和请求数据启用或禁用数据域功能。

配置检查
  1. 正如上述配置提到,启用DataRegion的第一个条件就是配置了 /prefix

  2. 其次HttpMethod方法必须是下边几个值:

    • GET

    • POST

    • OPTIONS

  3. 最后系统会检查请求路径是否以配置路径开始,如上述配置中像 /oauth/login, /oauth/authorize, /oauth/token 都不满足以 /api/ 为前缀。

输入检查

数据域是和安全视图配合操作的,若前边资源请求流程中无法读取到视图信息,那么数据域功能就会直接跳过不执行。输入检查是基于 DataBound 对象转换的JSON对象(输入 matrix):

boolean isRegion(final JsonObject matrix)

它的结构可参考 用户登录缓存,参考下边的输入检查表格,按顺序检查:

规则 结果

matrix为空

禁用数据域

matrix中projection有值

启用数据域

matrix中credit有值

启用数据域

matrix中rows有值

启用数据域

matrix中criteria有值

启用数据域

matrix中seeker有值

根据视图数据的存在与否启用和禁用数据域,无视图数据禁用

其他

上述规则都不满足,直接禁用数据域

Before流程

Before流程主要做三件事:

  • 访问者流程:若启用了访问者流程则重新提取视图资源信息修改 matrix

  • projection修改(先知):根据最终视图数据修改projection。

  • criteria修改(先知):根据最终视图数据修改criteria。

完整流程图如下:

zbac region before

After流程

After流程主要做四件事:

  • 访问者流程:若启用了访问者流程则重新提取视图资源信息修改 matrix

  • record过滤:处理单数据记录的过滤信息。

  • rows过滤:针对集合数据集执行行筛选。

  • collection过滤:针对集合执行列过滤。

zbac region after

响应特殊数据

数据域经过了上述流程之后会对响应数据执行最终更改,在响应数据中会追加如下属性执行特殊操作:

属性 含义

__acl

基于查询语法的ACL控制,主要针对表单记录操作的ACL响应限制。

__qr

基于查询语法的视图控制,返回 __qr 级核心操作,存储了当前视图中定义的查询条件(只针对视图,不针对访问者)。

管理端

设计原则

前文讨论了Zero权限框架中的 消费端,本章节开始讨论权限框架的 管理端,管理端的主要目的是为您定制所需要的核心配置数据,并且通过交互式界面直接为用户执行权限、视图、访问者的授权工作,管理端入口在Zero Extension框架中有三个:

  • 角色管理 → 权限设置

  • 权限设置 → 角色授权 → 选择某个角色

  • 权限设置 → 用户特权(现阶段版本还未放出来)

管理端的主体是 角色,所以管理端的所有操作都是角色级的,只是在设计过程中管理端遵循如下设计原则:

  1. 管理端界面使用 S_PACKET / S_PATH 进行配置,由于所有管理端都只作用于权限、视图、访问者,所以管理模式可以统一。

  2. 管理界面分 维度管理配置管理,维度管理通常用于定义:按什么方式管理,而配置管理则着眼于管理什么。

  3. 管理配置可直接扩展,对整个权限管理部分而言,多一种管理界面只是多了一条 S_PACKET 数据配置记录(前端可直接解析实现完整管理模式)。

  4. 现阶段开放的仅:菜单管理流程管理 两部分内容。

由于管理端的配置相对更加复杂,所以本文以现阶段已经配置好的 菜单管理流程管理 为例讲解管理端的配置细节。

区域基础

区域(旧版本称为:资源路径)位于 S_PATH 表结构中,它定义了目前权限管理的区域板块,如下图:

zero p admin region

在权限管理中,区域会衍生横向的页签,每一个页签由一个区域配置来实现,只是该区域配置中可以包含子区域。权限管理 端的主要目的是针对 S_VIEWS_VISITANT 中的数据执行角色级和用户级的增删改,所以 S_PATH 中的定义相对比较抽象。

  • 非区域模式:在非区域模式下 S_PATH 中的 PARENT_ID 通常是 NULL 值,此时的管理分为两种:

    • 带维度定义的管理:按照某种维度分类之后执行管理。

    • 不带维度定义的管理:直接平行管理,最简单的操作。

  • 区域模式:在区域模式下 S_PATH 通常是一个树型结构,顶层区域(页签)这一级的 PARENT_ID 通常是 NULL 值,但子区域是挂在主区域上的。

非区域模式适合管理1个资源的场景,如菜单管理,仅管理菜单读取资源的信息就可以完成。区域模式则适合管理N个资源的场景,如流程管理,除了管理流程的OP资源以外,还有流程最右上角的QBE的管理。最终遵循的原则是:所有的子区域负责资源管理的执行,非子区域主要负责区域的调度,非区域模式其实就是特殊区域管理(子区域和父区域)的等价模式。

此处使用 区域 的目的是接口最终会带有 authority region 关键字。

完整结构

管理端执行配置化管理的整体拓扑图如下:

zero p admin arch

维度定义

维度定义在Zero权限管理中是一个比较新的概念,它定义了不同场景的管理行为:

  • 比如管理菜单:需要依赖菜单分类之后执行管理,而不是将所有菜单统一管理,这种模式下需定义菜单的 分类维度

  • 比如流程管理:需要依赖选择某个流程之后执行相关管理(包括QBE列表、OP操作),这种模式下维度定义需定义流程的 分类维度

  • 比如模型管理:需要依赖选择某个模型之后执行相关管理,这种模式下维度定义需定义模型的 分类维度

归根到底,管理端的主要操作是针对 S_VIEWS_VISITANT 中定义的内容执行增删改以保证配置生效,作用于不同角色访问不同资源的安全视图,参考上述拓扑图就可以完成所有管理端的配置操作,接下来以菜单管理/流程管理为范例,来讲解管理端的详细配置。

全区域元数据

初始化区域

Zero权限管理中进入界面之后会首先调用 GET /api/authority/region/:type 接口提取所有合法区域信息。

注:此处虽然仅提供了参数 type,实际构造查询 S_PATH 的SQL语句如下:

WHERE RUN_TYPE = 'ROLE' AND SIGMA = 'xxx' AND PARENT_ID IS NULL

该接口的响应格式是一个JsonArray,其中菜单管理区域响应格式如下:

{
    "key": "f37f66c0-40a9-4816-ade2-4230f4ee045f",
    "name": "菜单设置",
    "code": "rule.menu",
    "phase": "EAGER",
    "runType": "ROLE",
    "dmType": "FLAT",
    "dmComponent": "io.vertx.mod.rbac.ruler.HSDimNorm",
    "dmConfig": {
        "...": "(略)"
    },
    "ui": "HxSite",
    "uiSort": 1,
    "uiType": "DAO",
    "uiConfig": {
        "...": "(略)"
    },
    "uiCondition": {
        "...": "(略)"
    },
    "uiComponent": "io.vertx.mod.rbac.ruler.HSUiNorm",
    "uiSurface": {
        "...": "(略)"
    },
    "sigma": "Qxw5HDkluJFnAPmcQCtu9uhGdXEiGNtP",
    "language": "cn",
    "active": true,
    "createdBy": "zero-environment",
    "label": "菜单设置"
}

上述结构的构造来自于 S_PATH 中的配置,其中只有一个属性比较特殊,ui 属性来自于配置 uiSurface 中的 webComponent 属性,此处对照 S_PATH 对各个属性再加以说明(此处只枚举特殊属性,不枚举Zero规范下的标准化字段如 sigma, active 等)。

属性 子属性(Json格式内定义) 数据列 含义

name

NAME

菜单权限管理标题

label

来源于 name

code

CODE

菜单权限管理的系统编码,该系统编码会辅助后期生成配置数据和取值专用数据。

phase

PHASE

UI数据的提取阶段。

runType

RUN_TYPE

当前配置管理类型,目前只支持:ROLE(角色级)。

mapping

MAPPING

从 dm → ui 的转换,用户触发维度操作时,将维度数据转换成界面数据的查询条件专用映射配置。

dmType

DM_TYPE

维度分组类型。

dmComponent

DM_COMPONENT

维度数据提取专用组件。

dmCondition

DM_CONDITION

维度数据提取时的查询条件,遵循查询引擎专用语法,会被 dmComponent 消费。

dmConfig

items

DM_CONFIG

(前端)维度界面配置 / items用于描述维度数据的呈现基础规则。

webAction

(前端)维度界面配置 / webAction负责维度数据操作触发的连接(右上角按钮事件配置)。

ui

来源于 uiSurface 中的 webComponent 属性,当前管理界面的主要组件。

uiSort

UI_SORT

当前区域的排序,最终在页签中呈现的顺序按 uiSort 进行排列。

uiType

UI_TYPE

界面数据提取类型。

uiConfig

UI_CONFIG

界面数据配置专用,会直接被 uiComponent 在后端消费。

uiComponent

UI_COMPONENT

界面数据配置组件,用于提取界面数据以及处理专用。

uiCondition

UI_CONDITION

界面数据查询模板,遵循 QR 查询引擎专用语法。

uiSurface

webComponent

UI_SURFACE

(前端)界面配置数据 / webComponent,界面呈现主要组件。

webBind

(前端)界面配置数据 / 前端和资源绑定的基础规则,某个组件和资源编码绑定,不同组件处理不同资源。

webTree

(前端)界面配置数据 / 界面数据以树模型呈现时,提供树型组件参数。

webData

(前端)界面配置数据 / 界面数据流规则定义。

维度属性 DM / 界面属性 UI

Zero权限管理框架中有两个核心前缀:

  • DM:维度配置前缀,用于配置当前管理界面所需的维度信息(按某种维度分类管理规则),且不同前端组件对维度定义有所区别。

  • UI:界面配置前缀,用于配置当前管理界面交互式组件专用界面信息。

最初您可能对 DM 以及 UI 的概念不太理解,但一旦配置过一个完整的权限管理模块(如菜单管理、流程管理),您就逐步理解二者的区别和用法了,权限管理界面大部分内容是通过配置的手段实现,并非使用代码开发的方式,所以才会有复杂的 DM / UI 架构来辅助您生成不同的界面。

特殊属性说明
  1. 阶段属性 phase:阶段属性和权限管理其他位置的取值一致:EAGER / LAZY 两种:

    • EAGER:加载区域元数据时同时提取界面数据资源,如菜单管理中系统所有菜单需要在加载界面时全部提取出来,才可以执行配置,这种模式是EAGER模式。

    • LAZY:加载区域元数据时不提取界面数据,这种一般用于 动态建模,提取维度数据之后,借用维度数据接口执行二次加载,此时界面数据才会被提取出来。

  2. 运行属性 runType:该属性作为查询条件使用,现阶段只支持 ROLE 一种(角色级管理),后续可支持多种,取值对应到 S_VIEW 中的核心字段 OWNER_TYPE,目前有三种:ROLE / USER / DIRECTORY

  3. 映射属性 mapping:映射属性十分重要,考虑这样一个场景:左侧有一个模型分类的树,来自于字典表 X_CATEGORY,当界面出现字典树时,用户会点击某个类型的字典去过滤所有模型数据,如:服务器下的路由器管理 等,这种场景下要将字典记录 category 转换成模型记录的查询条件实现过滤,它们之间表结构通常会如下图,所以映射属性就可以辅助我们完成从 DM 到 UI 的转换(此种转换通常适用于维度数据为动态数据的场景)。

    0

  4. 维度类型 dmType:定义维度数据的提取模式

    含义

    NONE

    无维度数据提取,直接平行定义

    FLAT

    列表型数据提取,只划分区域执行列表型提取操作

    TREE

    树型数据提取

    FOREST

    森林型数据提取,等价于 FLAT 和 TREE 两种模式综合

  5. 界面类型 uiType:定义界面数据的提取模式

    含义

    NONE

    无数据源,纯前端界面开发模式

    WEB

    静态数据专用,直接从 json 配置文件中解析界面数据完成数据提取

    DAO

    动态数据提取,使用静态模型的Dao层直接提取数据

    ATOM

    动态数据提取,使用动态模型的数据提取

    DEFINE

    自定义模式,组件使用模式,这种模式依赖 uiConfig 和 uiComponent 的自定义开发

  6. dmConfig中的 webAction 属性,该属性主要用于构造连接保存模型,通常会实现防重复加载、事件连接、点击加载等一连串前端事件流程,命名规范如下:

    • 连接点ID:__ID 作连接点前缀,触发 Ux.connectId 函数的按钮。

    • 被连接点ID:__CID 作被连接点前缀,Ux.connectId 函数参数 id 对应的按钮。

  7. uiSurface中的 webData 属性,该属性用于处理页面数据流,提供了数据的 初始化、读取、变更、提交 等核心数据流程,常用的几个属性如下:

    参数名 含义和值

    paramIn

    数据初始化时的默认数据信息。

    paramView

    视图相关数据,主要设置视图的两个核心维度 view, position ,不设置时使用默认值:DEFAULT / DEFAULT

    paramVisit

    初始化访问者 S_VISITANT 时的专用数据配置,一般新增访问者需要。

    paramDefault

    无值默认行为,ALL代表全选,NONE代表全部不选中,常用值为ALL。

    initializer

    初始化JS函数

    requester

    提交专用JS函数

单区域:元数据

Zero权限管理中在点击页签时(除开第一个页签)会调用 GET /api/authoriy/region-d/:key 接口提取单个区域的所有配置信息,它的响应格式如下:

{
    "group": {
        "...": "(略)"
    },
    "config": {
        "...": "(略)"
    },
    "ui": "HxSite",
    "key": "f37f66c0-40a9-4816-ade2-4230f4ee045f",
    "label": "菜单设置",
    "value": "rule.menu",
    "datum": {
        "...": "(略)"
    },
    "data": {
        "...": "(略)"
    }
}

上述结构是对单个区域执行过标准化的结构,其内容和含义如下:

节点名称 含义

group

维度处理专用数据

config

界面配置专用数据

datum

维度元数据定义,对应前一章节的 region 定义信息

data

界面显示专用数据

不论是全区域元数据还是单区域元数据中,都牵涉 webComponent 的配置,实际该配置是前端和后端配合操作的基础,目前支持的值如:

  • HxQueue:流程QBE列表配置组件

  • HxFlow:流程图配置组件

  • HxAction:操作配置组件

  • HxMenu:旧版菜单配置组件

  • HxSite:带应用维度菜单配置组件

您可以在Zero UI前端开发过程中拓展自己的权限管理专用组件,所有 Hx 前缀组件都是权限管理专用。

单区域:数据

Zero权限管理中在点击页签时除了提取元数据信息以外,还会调用 POST /api/authority/region-v/:key 接口提取单个区域的所有数据信息,数据信息响应格式如下:

{
    "res.menu.read": {
        "data": [
            {
                "h": {
                    "name": [...]
                },
                "q": {},
                "v": [],
                "view": "DEFAULT",
                "position": "DEFAULT",
                "virtual": false
            }
        ],
        "metadata": {
            "h": {
                "config": {},
                "mapping": {
                    "key": "name"
                }
            },
            "q": {},
            "v": {}
        }
    }
}

如果读取数据时存在多个资源,此处响应数据不止一个 key 属性,上述示例中的属性键为 res.menu.read,这个值就是资源 GET /api/menus 接口的资源编码(后端系统中 S_RESOURCE 表对应的 CODE 字段的值),菜单在执行过滤过程中,Zero系统中采用的是行筛选(ROWS 模式),所以最终存储在视图中的数据结构如:

{
    "name": [
        "...."
    ]
}

您可以从响应数据的 data 节点中捕捉到 h 类数据,而 metadata 中存储的则是转换规则,响应值的基本规则如:

属性 对应属性 含义

h

ROWS

h代表Horizon,行筛选。

v

PROJECTION

v代表Vertical,列筛选。

q

CRITERIA

q代表Query,查询引擎筛选。

In/Out预处理

参考下边的完整流程图,前边三个服务端接口已经讲解过,本章节主要讲解 In/Out 的数据预处理流程(棕色部分)的代码逻辑:

0

远程数据读取后,一般的 Hx 组件属性会包含如下核心结构:

属性 子属性 类型 含义

$anchors

props / JsonArray

被连接的按钮 __CID 主键,可能存在多个。

$initial

props / JsonObject

初始化值专用,一般为 owner / ownerType 信息。

$owner

props / JsonObject

当前拥有者信息,角色权限中则是角色信息,用户权限中则是用户信息。

$region

props / JsonObject

当前单区域元数据信息(来自 /api/authority/region-d/:key 接口,数据结构同上。

$bindValue

props / String

当前组件绑定的资源code,如菜单管理中该值是 res.menu.read。

config

webAction

props / JsonObject

当前组件的操作相关信息,同时包含 CID / ID 类按钮连接ID。

webBind

props / JsonObject

组件和资源绑定的基本信息,结构为:webComponent = resource 结构。

webComponent

props / String

当前组件名称(该属性已在外层消费)。

webData

props / JsonObject

组件绑定的数据流配置信息。

webTree

props / JsonObject

(非必须)维度部分设置树型菜单时的特殊配置。

webUi

props / JsonObject

对应后端的UI部分的配置信息。

data

props

当前组件的数据源信息,数据源会在预处理过程中执行 分组之后拉平 的计算。

$keySet

state / Set

当前组件为Checkbox类型时,用户交互时的选中值受控属性。

$keyDefault

state / Set

当前组件为Checkbox类型时,初始化进入界面时候的选中值集合。

$bindData

<resource>

state / JsonObject

当前组件使用(包括子组件)的所有数据信息。

$inited

h

state / JsonObject

当前组件消费的专用 ROWS 值。

q

state / JsonObject

当前组件消费的专用 CRITERIA 值。

v

state / JsonObject

当前组件消费的专用 PROJECTION 值。

上述所有结构中,并非所有组件都包含全部结构,尤其是基础数据 data 的结构相对浮动,但大部分结构是一致的,最终可达到所有组件一致性。

  • 从上边的流程以及数据结构可以知道,aclIn的预处理过程就是生成 state 状态中的所有值的过程。

  • 而 aclOut 的预处理流程则刚好和 aclIn 相反,是通过 $keySet / $keyDefault 生成最终请求的过程。

In预处理流程

In 的预处理流程会调用 aclRegionInit 函数,并构造前一小节提到的状态(state)中的所有数据信息。

aclRegionInit 执行流程如下( 注意aclIn是在哪个步骤被调用 ):

  1. 先检查和判断 $bindData 的合法性,从前边章节可以知道 $bindData 的数据结构为:resource = data(h,q,v) 的结构,当出现子组件时,该属性会因为继承的关系直接有值,此时就不用从远程调用 POST /api/authority/region-v/:key 接口提取数据,即:从远程加载单区域的数据是一次性行为,如果是父类组件,会一次性从远程加载所有绑定资源对应的数据信息(包括 S_VIEW 和 S_VISITANT)。

  2. 根据 config 中的两个属性 webBind / webComponent 计算当前组件需要使用的资源数据:

    {
        "HxSite": "res.menu.read"
    }

    $bindData 的结构是 resource = data(h,q,v) 的一个Map结构,而 webComponent 就是当前消费的组件名,最终计算的 dataKey 就是资源编码,提取的自然是资源数据。

    当前权限管理中暂时不支持在同一个面板中使用不同组件修改同一资源的情况,而且从实际经验看起来这种情况大概率是配置有问题,交互层会导致数据流混乱。后续版本中会根据实际需求拓展成绑定的资源信息不止一个的情况,由于JS是弱类型,此处后续可使用类似如下:

    {
        "HxSite": [
            "res.menu.read",
            "res.xxxx",
            "..."
        ]
    }

    这一步完成之后,状态中会出现 $inited 变量(初始化数据变量),其中包括:h, q, v, virtual, view, position 等。

  3. (AOP)执行 keyFn2 二阶函数重新处理 $keySet,该步骤通常是 过滤、转换、计算 等。

  4. 调用 aclIn 函数执行标准化计算 $keySet,并根据 $keySet 计算 $keyDefault 集合。

上述流程中最后一步就会执行 aclIn 函数,它会直接调用 webData 中配置的 initializer,此处需要您关注的是 initializer 的第二参数 param 的数据结构:

属性 含义

webValue

(数据)直接绑定前边步骤计算出的 $inited 变量。

webData

(配置)从 config 中提取配置 webData 并生成该属性信息。

webFn

(行为)直接绑定 aclRegionInit 最外层传入的第二参,该参数是一个JsonObject类型,但包含了类似 key2Fn, preFn 等AOP层的基础函数,带2表示2阶函数

Out预处理流程

Out 预处理流程是在最终点击 保存 按钮之前触发,主要目的是生成最终接口所需的数据信息,该流程的输入就是 $keySet

Zero前端组件中有一个不成文的规定,就是通常使用多选操作时,会生成 $keySet / $keyDefault 两个集合保存在状态中,$keySet 是随时变化的,记录了用户的操作最终结果,$keyDefault 只会在第一次进入界面时设置。

Out 预处理流程会直接调用 aclOut 函数,它会直接调用 webData 中配置的 requester,此处需要您关注的依旧是 requester 的第二参数 param 的数据结构:

属性 含义

webValue

(数据)直接绑定前边步骤计算的 $inited 变量。

webSelected

用户操作结果。

webData

(配置)从 config 中提取配置 webData 并生成该属性信息。

webOption

(外层配置)绑定外层传入的选项信息。

详解initializer

从示例配置中可以知道,菜单中的 initializer 配置的值为 IN_H,此处先列出目前版本拥有的所有 initializer 函数:

函数名 标记 含义

IN_H

H-视图

提取视图中的 rows 数据。

IN_HV

H-视图,V-访问者

提取视图访问者中的 dmRow 数据。

IN_HV_BY

H-视图,V-访问者

带条件提取视图访问者中的 dmRow。

此处先看看菜单级的 initializer 具体做了什么事(解析 IN_H ),菜单的元数据结构如下( props → data ):

{
    "SIDE-MENU": [],
    "TOP-MENU": [],
    "NAV-MENU": [],
    "...": "(略)"
}

initializer 函数执行流程在几个选择中大同小异,下边以 IN_H 函数内部流程讲解一下流程的大概细节,其他流程略微有些差异(但差异不大)。

  1. initializer 首先构造数据源(本例中是所有可选中菜单记录),数据源最终返回必须是一个 JsonArray 的数组结构(最少在目前的权限管理中如此),它的计算基础就是属性 props 中的 data 数据节点:

    • 如果 data 是 JsonArray 类型,则直接将它返回成可操作的数据源节点。

    • 如果 data 是 JsonObject 类型,在当前版本中只会出现一种情况(如菜单管理),分组数组类型,如示例代码段,此时数据源会执行 Ux.elementFlap 动作将所有的 value 连接到一起形成一个组合的新数组。

  2. 根据标识 v, q, h 提取值中定义的 mapping 映射信息,参考前文的预处理参数,提取路径为:webValue → metadata → ? → mapping,其中 ? 表示传入的标识,在 IN_H 类型函数中,标识当然是 h,如果没有在后端配置任何相关的 mapping 性质,则会生成下边代码段。

    {
        "key": "key"
    }
  3. 根据数据源和映射配置(前两步结果)生成最终的数据集,并执行 $keySet / $keyDefault 的标准化流程。

  4. 设置默认选中行为(paramDefault 配置),一般默认选中用于解决:无数据模式 究竟初始化时没有数据是全部选中还是全部不选。

详解requester

requester 在流程图中和 initializer 是两个相反的操作,此处列出目前版本所有存在的 requester 函数:

函数名 标记 含义

OUT_C

C-子组件

根据子组件配置生成子组件单独的视图数据提交数据信息。

OUT_H

H-视图

根据 $keySet / $keyDefault 直接计算当前组件的提交数据,ROWS类型。

requester 的代码流程这里就不列举,大部分 OUT 类的执行流程都有所差异,不论哪种流程,最终都会构造数据发送到接口:POST /api/authority/region/:value 中,它的请求格式通常如下:

{
    value: "rule.menu",
    $body: {
        "owner": "xxx",
        "ownerType": "ROLE",
        "resource": {
            "res.menu.read": {
                "criteria": {},
                "projection": [],
                "rows: {},
                "view": "DEFAULT",
                "projection": "DEFAULT",
                "uid": "xxx",
                "visitant": [
                    {
                        "identifier": "xxx",
                        "mode": "xxx",
                        "phase": "xxx",
                        "seekKey": "xxx",
                        "type": "xxx",
                        "dmRow": {}
                    }
                ]
            }
        }
    }
}

上述请求格式是标准化格式:

  • value:对应到 S_PATH 表中的 CODE,管理端区域编码,每次保存数据会直接更新该区域下所有关联资源的视图以及访问者信息。

  • owner/ownerType:用于定位单个角色或用户相关信息。

  • resource:该节点为核心数据节点,通常格式为 resourceCode = data 的结构,此处JsonObject键是资源编码,可标识唯一资源信息。

  • resource 对应数据节点:数据节点中第一层是 单视图 相关信息,包含了视图相关信息,两个特殊节点如下:

    • uid:该节点为加密专用节点,前后端会对照计算以确定当前权限更新信息是合法的。

    • visitant:用于设置当前更新视图相关的访问者信息,访问者信息会同步到后端。

该接口会执行 添加/保存 两个核心动作,若视图或访问者不存在,只要数据结构合法,最终都会同步到后端数据库中。目前的权限管理版本还不牵涉一个资源多个视图的更新,从实际使用过程可以知道,一个资源多个视图的更新基本不会存在于权限管理流程,这种场景在现阶段没有需求,所以资源关联数据格式依旧是 JsonObject 格式。

父子嵌套调用

父子调用过程主要存在于流程管理中,流程管理的基本布局如下:

0

上述结构图中,出现了父子级区域,所以本章主要讲解父子级区域的核心流程,父子级区域同样会走 initializerrequester 的基础流程,中间部分的内容是不会变化的,但有几个点需要注意:

  1. 子组件执行 requester 的时间并非是提交前,而是触发 Checkbox 时,当子组件触发了 Checkbox 中的 onChange 函数,子类的 requester 流程就直接启动,最终会生成完整的请求结构,并且将结构中的 resource 回传到父类回调函数中。

  2. 父组件状态 state$keyChild 的完整结构就是 resource = data,但和不带父子结构的内容有所差异的地方在于,此处的 resource 包含了当前父区域下所有子区域关联的资源,一般和 webBind 中定义的资源保持一致。

  3. OUT_C 表面上看起来是一个 requester 函数,实际是一个组合函数,它负责将所有合法的 $keyChild 中存在的资源集体打包,最终构造发送到后端接口中的标准化请求数据。

理解清楚了 $keyChild 中的数据结构,就等价于完全理解了父子级调用关系。

示例/格式

S_PATH中的DM_CONFIG
{
    "items": {
        "root": "BAG-MENU",
        "exclude": [
            "zero.desktop"
        ],
        "children": [
            "SC-MENU,服务目录菜单",
            {
                "key": "NAV-MENU",
                "title": "主页快捷菜单",
                "value": [
                    "NAV-MENU",
                    "REPO-MENU"
                ]
            },
            "SIDE-MENU,应用主菜单"
        ]
    },
    "webAction": {
        "icon": "upload",
        "text": "保存",
        "type": "primary",
        "key": "__ID_MENU_SAVE",
        "connectId": "__CID_MENU_SAVE"
    }
}

上述配置中的 items 节点是 HxSite 专用的配置信息,此处就不介绍相关内容,而 webAction 定义了页面专用按钮:连接点和被连接点的按钮配置相关信息。另外一种 items 的配置如下:

    "items": {
        "dao": "cn.vertxup.workflow.domain.tables.daos.WFlowDao"
    }

这种情况下需要结合 DM_CONDITION 中的查询条件来实现维度数据的拉取工作。

S_PATH中的DM_MAPPING
{
    "name": "label",
    "code": "key",
    "definitionKey": "value",
    "type": "type"
}
S_PATH中的UI_CONDITION
{
    "sigma": "`${sigma}`",
    "appId": "`${appId}`",
    "": true
}
S_PATH中的UI_CONFIG
{
    "dao": "cn.vertxup.ambient.domain.tables.daos.XMenuDao",
    "output": {
        "group": "type"
    }
}

此处的 output 可以包含不同的模式,现阶段主要支持两种:

  1. 直接以 JsonArray 的方式返回数据到前端。

  2. 以按照某个维度字段分组的方式将原始 JsonArray 计算生成一个 JsonObject,最终返回到前端。( initializerrequester 会在前端自动计算数据源,所以不用过于担心结构问题。)

S_PATH中的UI_SURFACE
{
    "webComponent": "HxSite",
    "webBind": {
        "HxSite": "res.menu.read"
    },
    "webTree": {
        "parent": "parentId",
        "title": "text",
        "text": "text",
        "value": "name",
        "sort": "order"
    },
    "webData": {
        "paramView": {
            "view": "DEFAULT",
            "position": "DEFAULT"
        },
        "paramDefault": "ALL",
        "initializer": "IN_H",
        "requester": "OUT_H"
    }
}

带父子级的 UI_SURFACE 参考如下(父类):

{
    "webComponent": "HxFlow",
    "webChildren": [
        {
            "component": "HxQueue",
            "key": "rule.flow.queue"
        },
        {
            "component": "HxAction",
            "key": "rule.flow.action"
        }
    ],
    "webBind": {
        "HxQueue": "res.views.fetch",
        "HxAction": "res.op.by.control"
    },
    "webWelcome": {
        "header": {
            "label": "选择流程",
            "value": "流程编码"
        },
        "alert": {
            "message": "流程权限说明",
            "description": [
                "您可以在当前页面设置不同角色针对不同流程的相关权限。",
                "`保存`操作只会保存当前选择流程的权限配置。",
                "`保存所有`操作会一次性保存当前页面所有设置过的流程权限配置。",
                "视图权限用于设置该角色在不同列表可访问的视图可见性以及视图的特殊定制。",
                "操作权限用于设置该角色在不同流程节点可执行的按钮集合以及特殊定制。"
            ]
        }
    },
    "webData": {
        "requester": "OUT_C"
    }
}

一般如果带有父组件,组件本身的触发会在父类实现,而父类的 webData 就不配置和底层资源相关的任何数据以及 initializer,如上边片段可知,仅提供 requester 部分做输出数据组合就可以了,这种场景下父类做的主要工作是 组合数据

带资源访问者的子类使用的 UI_SURFACE 配置:

{
    "webNode": {
        "event": "节点值",
        "name": "流程节点名称",
        "action": "操作列表",
        "empty": "请选择您要设置权限的流程节点"
    },
    "webData": {
        "paramIn": {
            "workflow": "PROP:workflow.value",
            "event": "STATE:node.event"
        },
        "paramView": {
            "position": ":workflow",
            "view": ":event"
        },
        "paramVisit": {
            "phase": "EAGER",
            "mode": "REPLACE",
            "identifier": "ui.op",
            "type": "OP",
            "seekKey": ":workflow/:event/DEFAULT"
        },
        "paramDefault": "ALL",
        "initializer": "IN_HV_BY",
        "requester": "OUT_H"
    }
}

上述配置片段是目前已经存在的片段信息,提供片段仅让您有所参考,如此您就可以彻底理解 管理端 的基本配置了,扩展开发管理端也变得相对容易很多,剩余关于权限在请求流程中的内容后续补充文档来加以说明。